// Copyright (c) 2024 CHANGLEI. All rights reserved.

import 'dart:math' as math;

import 'package:charts_grasp/src/chart/axis/axis.dart';
import 'package:charts_grasp/src/chart/axis/multi_level_labels.dart';
import 'package:charts_grasp/src/chart/chart_series/series.dart';
import 'package:charts_grasp/src/chart/chart_series/series_renderer_properties.dart';
import 'package:charts_grasp/src/chart/chart_series/xy_data_series.dart';
import 'package:charts_grasp/src/chart/common/cartesian_state_properties.dart';
import 'package:charts_grasp/src/chart/utils/enum.dart';
import 'package:charts_grasp/src/chart/utils/helper.dart';
import 'package:charts_grasp/src/common/event_args.dart';
import 'package:flutter/material.dart';

@immutable
class CategoryAxis extends ChartAxis {
  CategoryAxis({
    super.name,
    super.isVisible,
    super.title,
    super.axisLine,
    this.arrangeByIndex = false,
    super.rangePadding,
    this.labelPlacement = LabelPlacement.betweenTicks,
    super.edgeLabelPlacement,
    super.labelPosition,
    super.tickPosition,
    super.labelRotation,
    super.labelIntersectAction,
    super.labelAlignment,
    super.isInversed,
    super.opposedPosition,
    super.minorTicksPerInterval,
    super.maximumLabels,
    super.majorTickLines,
    super.minorTickLines,
    super.majorGridLines,
    super.minorGridLines,
    super.labelStyle,
    super.plotOffset,
    super.zoomFactor,
    super.zoomPosition,
    super.interactiveTooltip,
    this.minimum,
    this.maximum,
    super.interval,
    this.visibleMinimum,
    this.visibleMaximum,
    super.crossesAt,
    super.associatedAxisName,
    super.placeLabelsNearAxisLine,
    super.plotBands,
    super.desiredIntervals,
    super.rangeController,
    super.maximumLabelWidth,
    super.labelsExtent,
    super.autoScrollingDelta,
    super.borderWidth,
    super.borderColor,
    super.axisBorderType,
    super.multiLevelLabelFormatter,
    super.multiLevelLabelStyle,
    List<CategoricalMultiLevelLabel>? super.multiLevelLabels,
    super.autoScrollingMode,
    super.axisLabelFormatter,
  });

  final LabelPlacement labelPlacement;

  final bool arrangeByIndex;

  final double? minimum;

  final double? maximum;

  final double? visibleMinimum;

  final double? visibleMaximum;

  @override
  bool operator ==(Object other) {
    if (identical(this, other)) {
      return true;
    }
    if (other.runtimeType != runtimeType) {
      return false;
    }

    return other is CategoryAxis &&
        other.name == name &&
        other.isVisible == isVisible &&
        other.title == title &&
        other.axisLine == axisLine &&
        other.arrangeByIndex == arrangeByIndex &&
        other.rangePadding == rangePadding &&
        other.labelPlacement == labelPlacement &&
        other.edgeLabelPlacement == edgeLabelPlacement &&
        other.labelPosition == labelPosition &&
        other.tickPosition == tickPosition &&
        other.labelRotation == labelRotation &&
        other.labelIntersectAction == labelIntersectAction &&
        other.labelAlignment == labelAlignment &&
        other.isInversed == isInversed &&
        other.opposedPosition == opposedPosition &&
        other.minorTicksPerInterval == minorTicksPerInterval &&
        other.maximumLabels == maximumLabels &&
        other.majorTickLines == majorTickLines &&
        other.minorTickLines == minorTickLines &&
        other.majorGridLines == majorGridLines &&
        other.minorGridLines == minorGridLines &&
        other.labelStyle == labelStyle &&
        other.plotOffset == plotOffset &&
        other.zoomFactor == zoomFactor &&
        other.zoomPosition == zoomPosition &&
        other.interactiveTooltip == interactiveTooltip &&
        other.minimum == minimum &&
        other.maximum == maximum &&
        other.interval == interval &&
        other.visibleMinimum == visibleMinimum &&
        other.visibleMaximum == visibleMaximum &&
        other.crossesAt == crossesAt &&
        other.associatedAxisName == associatedAxisName &&
        other.placeLabelsNearAxisLine == placeLabelsNearAxisLine &&
        other.plotBands == plotBands &&
        other.desiredIntervals == desiredIntervals &&
        other.rangeController == rangeController &&
        other.maximumLabelWidth == maximumLabelWidth &&
        other.labelsExtent == labelsExtent &&
        other.autoScrollingDelta == autoScrollingDelta &&
        other.axisBorderType == axisBorderType &&
        other.borderColor == borderColor &&
        other.borderWidth == borderWidth &&
        other.multiLevelLabelStyle == multiLevelLabelStyle &&
        other.multiLevelLabels == multiLevelLabels &&
        other.multiLevelLabelFormatter == multiLevelLabelFormatter &&
        other.autoScrollingMode == autoScrollingMode &&
        other.axisLabelFormatter == axisLabelFormatter;
  }

  @override
  int get hashCode {
    final List<Object?> values = <Object?>[
      name,
      isVisible,
      title,
      axisLine,
      arrangeByIndex,
      rangePadding,
      labelPlacement,
      edgeLabelPlacement,
      labelPosition,
      tickPosition,
      labelRotation,
      labelIntersectAction,
      labelAlignment,
      isInversed,
      opposedPosition,
      minorTicksPerInterval,
      maximumLabels,
      majorTickLines,
      minorTickLines,
      majorGridLines,
      minorGridLines,
      labelStyle,
      plotOffset,
      zoomFactor,
      zoomPosition,
      interactiveTooltip,
      minimum,
      maximum,
      interval,
      visibleMinimum,
      visibleMaximum,
      crossesAt,
      associatedAxisName,
      placeLabelsNearAxisLine,
      plotBands,
      desiredIntervals,
      rangeController,
      maximumLabelWidth,
      labelsExtent,
      autoScrollingDelta,
      axisBorderType,
      borderColor,
      borderWidth,
      multiLevelLabelStyle,
      multiLevelLabels,
      multiLevelLabelFormatter,
      autoScrollingMode,
      axisLabelFormatter,
    ];
    return Object.hashAll(values);
  }
}

class CategoryAxisRenderer extends ChartAxisRenderer {
  CategoryAxisRenderer(CategoryAxis categoryAxis, CartesianStateProperties stateProperties) {
    _axisDetails = CategoryAxisDetails(categoryAxis, this, stateProperties);
    AxisHelper.setAxisRendererDetails(this, _axisDetails);
  }

  late CategoryAxisDetails _axisDetails;

  @override
  void calculateVisibleRange(Size availableSize) {
    _axisDetails.setOldRangeFromRangeController();
    _axisDetails.visibleRange = _axisDetails.stateProperties.rangeChangeBySlider &&
            _axisDetails.rangeMinimum != null &&
            _axisDetails.rangeMaximum != null
        ? VisibleRange(_axisDetails.rangeMinimum, _axisDetails.rangeMaximum)
        : VisibleRange(_axisDetails.actualRange!.minimum, _axisDetails.actualRange!.maximum);
    _axisDetails.visibleRange!.delta = _axisDetails.actualRange!.delta;
    _axisDetails.visibleRange!.interval = _axisDetails.actualRange!.interval;
    bool canAutoScroll = false;
    if (_axisDetails._categoryAxis.autoScrollingDelta != null &&
        _axisDetails._categoryAxis.autoScrollingDelta! > 0 &&
        !_axisDetails.stateProperties.isRedrawByZoomPan) {
      canAutoScroll = true;
      _axisDetails.updateAutoScrollingDelta(_axisDetails._categoryAxis.autoScrollingDelta!, this);
    }
    if ((!canAutoScroll || (_axisDetails.stateProperties.zoomedState ?? false)) &&
        !(_axisDetails.stateProperties.rangeChangeBySlider && !_axisDetails.stateProperties.canSetRangeController)) {
      _axisDetails.setZoomFactorAndPosition(this, _axisDetails.stateProperties.zoomedAxisRendererStates);
    }
    if (_axisDetails.zoomFactor < 1 ||
        _axisDetails.zoomPosition > 0 ||
        (_axisDetails.axis.rangeController != null && !_axisDetails.stateProperties.renderingDetails.initialRender!)) {
      _axisDetails.stateProperties.zoomProgress = true;
      _axisDetails.calculateZoomRange(this, availableSize);
      if (_axisDetails.axis.rangeController != null &&
          _axisDetails.stateProperties.isRedrawByZoomPan &&
          _axisDetails.stateProperties.canSetRangeController &&
          _axisDetails.stateProperties.zoomProgress) {
        _axisDetails.stateProperties.rangeChangedByChart = true;
        _axisDetails.setRangeControllerValues(this);
      }
    }
    _axisDetails.setZoomValuesFromRangeController();
  }

  @override
  void applyRangePadding(VisibleRange range, num? interval) {
    ActualRangeChangedArgs rangeChangedArgs;
    if (_axisDetails._categoryAxis.labelPlacement == LabelPlacement.betweenTicks) {
      range.minimum -= 0.5;
      range.maximum += 0.5;
      range.delta = range.maximum - range.minimum;
    }

    if (!(_axisDetails._categoryAxis.minimum != null && _axisDetails._categoryAxis.maximum != null)) {
      _axisDetails.applyRangePaddings(this, _axisDetails.stateProperties, range, interval!);
    }

    calculateVisibleRange(Size(_axisDetails.rect.width, _axisDetails.rect.height));

    if ((_axisDetails._categoryAxis.visibleMinimum != null || _axisDetails._categoryAxis.visibleMaximum != null) &&
        (_axisDetails._categoryAxis.visibleMinimum != _axisDetails._categoryAxis.visibleMaximum) &&
        (!_axisDetails.stateProperties.isRedrawByZoomPan)) {
      _axisDetails.stateProperties.isRedrawByZoomPan = false;
      _axisDetails.visibleRange!.minimum =
          _axisDetails.visibleMinimum ?? _axisDetails._categoryAxis.visibleMinimum ?? _axisDetails.actualRange!.minimum;
      _axisDetails.visibleRange!.maximum =
          _axisDetails.visibleMaximum ?? _axisDetails._categoryAxis.visibleMaximum ?? _axisDetails.actualRange!.maximum;
      if (_axisDetails._categoryAxis.labelPlacement == LabelPlacement.betweenTicks) {
        _axisDetails.visibleRange!.minimum = _axisDetails._categoryAxis.visibleMinimum != null
            ? (_axisDetails.visibleMinimum ?? _axisDetails._categoryAxis.visibleMinimum!) - 0.5
            : _axisDetails.visibleRange!.minimum;
        _axisDetails.visibleRange!.maximum = _axisDetails._categoryAxis.visibleMaximum != null
            ? (_axisDetails.visibleMaximum ?? _axisDetails._categoryAxis.visibleMaximum!) + 0.5
            : _axisDetails.visibleRange!.maximum;
      }
      _axisDetails.visibleRange!.delta = _axisDetails.visibleRange!.maximum - _axisDetails.visibleRange!.minimum;
      _axisDetails.visibleRange!.interval = interval == null
          ? calculateInterval(_axisDetails.visibleRange!, _axisDetails.axisSize)
          : _axisDetails.visibleRange!.interval;
      _axisDetails.zoomFactor = _axisDetails.visibleRange!.delta / (range.delta);
      _axisDetails.zoomPosition =
          (_axisDetails.visibleRange!.minimum - _axisDetails.actualRange!.minimum) / range.delta;
    }
    if (_axisDetails.chart.onActualRangeChanged != null) {
      rangeChangedArgs = ActualRangeChangedArgs(_axisDetails.name, _axisDetails._categoryAxis, range.minimum,
          range.maximum, range.interval, _axisDetails.orientation,);
      rangeChangedArgs.visibleMin = _axisDetails.visibleRange!.minimum;
      rangeChangedArgs.visibleMax = _axisDetails.visibleRange!.maximum;
      rangeChangedArgs.visibleInterval = _axisDetails.visibleRange!.interval;
      _axisDetails.chart.onActualRangeChanged!(rangeChangedArgs);
      _axisDetails.visibleRange!.minimum = rangeChangedArgs.visibleMin;
      _axisDetails.visibleRange!.maximum = rangeChangedArgs.visibleMax;
      _axisDetails.visibleRange!.delta = _axisDetails.visibleRange!.maximum - _axisDetails.visibleRange!.minimum;
      _axisDetails.visibleRange!.interval = rangeChangedArgs.visibleInterval;
      _axisDetails.zoomFactor = _axisDetails.visibleRange!.delta / (range.delta);
      _axisDetails.zoomPosition =
          (_axisDetails.visibleRange!.minimum - _axisDetails.actualRange!.minimum) / range.delta;
    }
  }

  @override
  void generateVisibleLabels() {
    num tempInterval = _axisDetails.visibleRange!.minimum.ceil();
    int position;
    String labelText;
    _axisDetails.visibleLabels = <AxisLabel>[];
    for (; tempInterval <= _axisDetails.visibleRange!.maximum; tempInterval += _axisDetails.visibleRange!.interval) {
      if (withInRange(tempInterval, _axisDetails)) {
        position = tempInterval.round();
        if (position <= -1 || (_axisDetails.labels.isNotEmpty && position >= _axisDetails.labels.length)) {
          continue;
        } else if (_axisDetails.labels.isNotEmpty) {
          labelText = _axisDetails.labels[position];
        } else {
          continue;
        }
        _axisDetails.triggerLabelRenderEvent(labelText, tempInterval);
      }
    }

    _axisDetails.calculateMaximumLabelSize(this, _axisDetails.stateProperties);
    if (_axisDetails._categoryAxis.multiLevelLabels != null &&
        _axisDetails._categoryAxis.multiLevelLabels!.isNotEmpty) {
      generateMultiLevelLabels(_axisDetails);
      calculateMultiLevelLabelBounds(_axisDetails);
    }
  }

  @override
  num calculateInterval(VisibleRange range, Size availableSize) => math
      .max(
          1,
          (_axisDetails.actualRange!.delta /
                  _axisDetails.calculateDesiredIntervalCount(
                      Size(_axisDetails.rect.width, _axisDetails.rect.height), this,))
              .floor(),)
      ;
}

class CategoryAxisDetails extends ChartAxisRendererDetails {
  CategoryAxisDetails(this._categoryAxis, ChartAxisRenderer axisRenderer, CartesianStateProperties stateProperties)
      : super(_categoryAxis, stateProperties, axisRenderer) {
    labels = <String>[];
  }

  late List<String> labels;

  late Rect rect;
  final CategoryAxis _categoryAxis;

  void findAxisMinMaxValues(
      SeriesRendererDetails seriesRendererDetails, CartesianChartPoint<dynamic> point, int pointIndex, int dataLength,
      [bool? isXVisibleRange, bool? isYVisibleRange,]) {
    if (_categoryAxis.arrangeByIndex) {
      pointIndex < labels.length ? labels[pointIndex] += ', ${point.x}'
          : labels.add(point.x.toString());
      point.xValue = pointIndex;
    } else {
      if (!labels.contains(point.x.toString())) {
        labels.add(point.x.toString());
      }
      point.xValue = labels.indexOf(point.x.toString());
    }
    point.yValue = point.y;
    setCategoryMinMaxValues(
        axisRenderer, isXVisibleRange!, isYVisibleRange!, point, pointIndex, dataLength, seriesRendererDetails,);
  }

  void _controlListener() {
    stateProperties.canSetRangeController = false;
    if (_categoryAxis.rangeController != null && !stateProperties.rangeChangedByChart) {
      updateRangeControllerValues(this);
      stateProperties.rangeChangeBySlider = true;
      stateProperties.redrawByRangeChange();
    }
  }

  void calculateRangeAndInterval(CartesianStateProperties stateProperties, [String? type]) {
    chart = stateProperties.chart;
    if (axis.rangeController != null) {
      stateProperties.rangeChangeBySlider = true;
      axis.rangeController!.addListener(_controlListener);
    }
    final Rect containerRect = stateProperties.renderingDetails.chartContainerRect;
    rect = Rect.fromLTWH(containerRect.left, containerRect.top, containerRect.width, containerRect.height);
    axisSize = Size(rect.width, rect.height);
    axisRenderer.calculateRange(axisRenderer);
    _calculateActualRange();
    if (actualRange != null) {
      axisRenderer.applyRangePadding(actualRange!, actualRange!.interval);
      if (type == null && type != 'AxisCross' && _categoryAxis.isVisible) {
        axisRenderer.generateVisibleLabels();
      }
    }
  }

  void _calculateActualRange() {
    if (min == null && max == null) {
      if ((stateProperties.zoomedState ?? false) || stateProperties.zoomProgress) {
        min ??= 0;
        max ??= 5;
      } else {
        min ??= 0;
        max ??= 0;
      }
    }
    if (min != null && max != null) {
      actualRange = VisibleRange(_categoryAxis.minimum ?? min, _categoryAxis.maximum ?? max);
      CartesianSeriesRenderer seriesRenderer;
      for (int i = 0; i < seriesRenderers.length; i++) {
        seriesRenderer = seriesRenderers[i];
        final SeriesRendererDetails seriesRendererDetails = SeriesHelper.getSeriesRendererDetails(seriesRenderer);
        if (((_categoryAxis.minimum != null || _categoryAxis.maximum != null) ||
                (stateProperties.zoomedState ?? false) ||
                stateProperties.zoomProgress) &&
            (actualRange!.maximum > seriesRendererDetails.dataPoints.length - 1) == true) {
          for (int i = labels.length; i < actualRange!.maximum + 1; i++) {
            labels.add(i.toString());
          }
        }
      }
      actualRange = VisibleRange(_categoryAxis.minimum ?? min, _categoryAxis.maximum ?? max);

      if ((actualRange!.minimum == actualRange!.maximum) && (_categoryAxis.labelPlacement == LabelPlacement.onTicks)) {
        actualRange!.maximum += 1;
      }
      actualRange!.delta = actualRange!.maximum - actualRange!.minimum;
      actualRange!.interval =
          _categoryAxis.interval ?? axisRenderer.calculateInterval(actualRange!, Size(rect.width, rect.height));
      actualRange!.delta = actualRange!.maximum - actualRange!.minimum;
    }
  }
}
